/*  
 * Copyright (C) 2011 Marta Spodymek
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *  
 */
package org.prometheuscode.web.feedback.binding;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;

import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;

import org.prometheuscode.web.feedback.ApplicationConfig;
import org.prometheuscode.web.feedback.ProcessingException;
import org.prometheuscode.web.feedback.domain.IFeed;
import org.prometheuscode.web.feedback.domain.IFeedInput;
import org.prometheuscode.web.feedback.file.FileInfo;
import org.prometheuscode.web.feedback.filestrategy.EncryptionType;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * This class is used for binding form input to application specific classes.
 * 
 * @author marta
 * 
 */
public class InputBinder implements IBinder {

    /*
     * This property indicates that given posted data should not be updated.
     */
    public final static String NOT_UPDATEABLE_STATUS = "%#--not-changed--#%";

    private ApplicationConfig appConfig;

    public InputBinder() {
    }

    /**
     * @return the appConfig
     */
    public ApplicationConfig getApplicationConfig() {
	return appConfig;
    }

    /**
     * @param appConfig
     *            the appConfig to set
     */
    @Autowired
    public void setApplicationConfig(ApplicationConfig appConfig) {
	this.appConfig = appConfig;
    }

    /**
     * Get inputs ready to save in data source.
     * 
     */
    @Override
    public IBindingHolder unbindInputs(HttpServletRequest request) {

	IBindingHolder holder = new BindingHolder();
	Map<String, String> inputs = null;
	List<FileInfo> files = null;
	if (request.getContentType().startsWith(
		EncryptionType.MULTIPART.getType())) {

	    files = new ArrayList<FileInfo>();
	    inputs = new HashMap<String, String>();
	    this.filterOutFilesAndParams(request, files, inputs);

	} else { /* we are having "normal" form post */
	    inputs = this.getSimpleParams(request);
	}

	holder.setFiles(files);
	holder.setInputs(inputs);

	return holder;
    }

    /**
     * Take IFeed object which contains user feedback and transform that
     * feedback into convenient map of values.
     * 
     * @param feed
     * @return
     */
    public Map<String, String> bindInputs(IFeed feed) {
	if (feed == null) {
	    return null;
	}
	Map<String, String> values = new HashMap<String, String>();
	Map<String, IFeedInput> inputs = feed.getUserFormInput();

	for (String inputName : inputs.keySet()) {
	    String inputValue = inputs.get(inputName).getAnswer();
	    values.put(inputName, inputValue);
	}

	return values;
    }

    /*
     * Take from multipart/form-data request posted params and files.
     */
    private void filterOutFilesAndParams(HttpServletRequest request,
	    List<FileInfo> files, Map<String, String> params) {

	Collection<Part> parts = null;
	try {
	    parts = request.getParts();

	    for (Part part : parts) {
		/*
		 * according to rfc http://www.ietf.org/rfc/rfc1867.txt
		 * content-type is optional on each part of MIME
		 * multipart/form-data. Most browsers do not include it for the
		 * form "normal" text input data part of multipart/form-data
		 * MIME message but they include it for file part of this MIME
		 * message. This can be used to filter out "normal" form
		 * elements and file inputs. (checked on IE, firefox, chrome,
		 * opera)
		 * 
		 * It should work most of the time, but might not always since
		 * Content-Type is optional header in each part of MIME
		 * multipart/form-data. However, if content-type is not
		 * recognized it is put in a HTTP request as:
		 * application/octet-stream
		 * 
		 * 
		 * We also don't want to get file if it is empty, size 0.
		 */
		if (part.getContentType() != null) {

		    if (part.getSize() != 0) {
			FileInfo info = new FileInfo();
			info.addInfo(FileInfo.CONTENT_TYPE_INFO,
				part.getContentType());
			info.addInfo(FileInfo.POSTED_NAME_INFO, part.getName());
			info.addInfo(FileInfo.FILE_NAME_INFO,
				this.getFileName(part));
			info.setInputStream(part.getInputStream());
			files.add(info);
		    } else { /* do not update file if there is not content */
			params.put(part.getName(), NOT_UPDATEABLE_STATUS);
		    }

		} else { /* it is "normal" form param */
		    String input = this.getSimpleInputContent(part
			    .getInputStream());
		    String inputName = part.getName();
		    if (params.containsKey(inputName)) {
			input = params.get(inputName)
				+ this.appConfig.getInputValueSeparator()
				+ input;
		    }
		    params.put(inputName, input);
		}

	    }

	} catch (Exception e) {
	    /*
	     * catch by Exception because this method should never be used in a
	     * context in which exceptions of request.getParts() will be thrown.
	     * 
	     * If this is false, then die.
	     */
	    throw new ProcessingException(
		    "Most likely IOException while reading a multipart file or something"
			    + "even more sinister! :O", e);
	}

    }

    /*
     * Get file name from part.
     */
    private String getFileName(Part part) {
	String fileName = "";
	String contentDispHeader = part.getHeader("Content-Disposition");
	int indexOfFileName = contentDispHeader.indexOf("filename");
	if (indexOfFileName >= 0) {
	    fileName = contentDispHeader.substring(indexOfFileName)
		    .replaceAll("filename=", "").replaceAll("[\\\"]", "");
	}

	return fileName;
    }

    /*
     * Parse multipart/form-data Part as InputStream and return string user
     * input value.
     */
    private String getSimpleInputContent(InputStream in) {
	char[] cb = new char[1024];
	int read = 0;

	/* TODO check encodings problems */
	InputStreamReader reader = new InputStreamReader(in);
	StringBuffer strBuffer = new StringBuffer();
	try {
	    while ((read = reader.read(cb)) > 0) {
		strBuffer.append(cb);
	    }
	} catch (IOException e) {
	    /* TODO choose proper exception, it is internal error */
	}

	return strBuffer.toString().trim();
    }

    /*
     * If request is not multipart/form-data get only first parameter with the
     * same key.
     */
    private Map<String, String> getSimpleParams(HttpServletRequest request) {
	Map<String, String[]> paramMap = request.getParameterMap();
	Map<String, String> simplifiedMap = new HashMap<String, String>();

	/*
	 * we can do this because generated form elements will have different
	 * name
	 */
	for (String key : paramMap.keySet()) {
	    String[] answers = paramMap.get(key);

	    String input = "";
	    if (answers.length > 1) {
		for (String part : answers) {
		    input = input + this.appConfig.getInputValueSeparator()
			    + part;
		}

		input = input.substring(1).trim();
	    } else {
		input = answers[0];
	    }

	    simplifiedMap.put(key, input);
	}

	return simplifiedMap;
    }

}
